"""Cellphone modem multiplexer. Handles SMS and GPRS functions."""

import logging
import os
import select
import socket
import signal
import sys
import time

import io.ioloop

import api
import modem
import msgdb
import pdu
import proto

#------------------------------------------------------------------------------
log = logging.getLogger('celld')

#------------------------------------------------------------------------------
def sec_sleep(seconds):
    # XXX/bowei -- This won't work on Windows
    select.select([],[],[], seconds)

#------------------------------------------------------------------------------
class Config:
    def __init__(self):
        self.db_file = None
        self.log_dir = None
        self.api_addr = None
        self.modems = {}

    def parse_args(self):
        self.db_file = Config.get_env_var('CELLD_DBFILE',
                                          '/var/spool/neon/celld/celld.db')
        self.log_dir = Config.get_env_var('CELLD_LOGDIR',
                                          '/var/log/neon/celld')
        self.api_addr = Config.get_env_var('CELLD_APIADDR',
                                                '/var/run/neon/celld.sock')
        self.max_sms_len = int(Config.get_env_var('CELLD_MAX_SMS_LEN', 159))

        for i in os.environ:
            if i.find('CELLD_MODEM_') != 0:
                continue
            desc = os.environ[i].split(',')
            name = desc[0]
            cfg = modem.ModemConfig(desc[1], modem.ModemConfig.str2baud(desc[2]))
            cfg.name = name
            self.modems[name] = cfg

    @staticmethod
    def get_env_var(name, default = 'XXXX'):
        # XXX/bowei -- refactor shared code
        if not name in os.environ:
            if default == 'XXXX':
                raise Exception('Missing configuration variable in ' + 
                                'environment: %s' % name)
            else:
                return default
        return os.environ[name].strip()

#------------------------------------------------------------------------------
class ModemProcess:
    """IPC protocol:
    Up:
    R\n     -- modem ready for next message
    r pdu\n -- received pdu

    Down:
    p\n     -- ping modem
    s pdu\n -- send pdu
    r\n     -- read SMS off the modem
    R\n     -- reset modem
    Q\n     -- kill process
    """
    def __init__(self, config, in_pipe, out_pipe):
        self.config = config
        self.in_pipe = in_pipe
        self.out_pipe = out_pipe
        self.modem = modem.Modem(self.config)
        self.ctrl = None
        self.log = logging.getLogger('modem.' + self.config.name)

    def write_parent(self, msg):
        self.out_pipe.write(msg + '\n')
        self.out_pipe.flush()

    def run(self):
        self.reset_modem()
        self.write_parent('R')
        
        while True:
            cmd = self.in_pipe.readline().strip()
            self.log.debug('got command: %s' % cmd)
            if cmd == '':
                self.log.info('master quit detected, exiting...')
                sys.exit(0)
            elif cmd[0] == 'p':
                self.ping()
            elif cmd[0] == 's':
                self.send_sms(cmd)
            elif cmd[0] == 'r':
                self.read_sms(cmd)
            elif cmd[0] == 'R':
                self.reset_modem()
            elif cmd[0] == 'Q':
                self.modem.close()
                sys.exit(0)

    def ping(self):
        self.log.debug('ping')
        self.ctrl.idle_ping()
        self.write_parent('p')

    def send_sms(self, cmd):
        # s <PDU>
        self.log.debug('send_sms')
        _, pdu = cmd.split(' ', 1)
        log.info('send_sms: PDU = %s' % pdu)
        self.ctrl.send_sms_msg(pdu)
        self.write_parent('R')

    def read_sms(self, cmd):
        self.log.debug('read_sms')
        l = self.ctrl.list_sms_msgs()
        log.debug('read_sms polled %d messages' % len(l))
        for _, pdu in l:
            log.info('recived PDU: %s' % pdu)
            self.write_parent('r %s' % pdu)
        for meta, _ in l:
            self.ctrl.delete_sms_msg(meta['index'])
            
    def reset_modem(self):
        self.log.debug('reset_modem')
        if self.ctrl is not None:
            self.ctrl = None
            self.modem.close()
        self.modem.open()
        self.ctrl = modem.ModemControl(self.modem)

    def close_all(self):
        self.log.info('closing comm pipes')
        self.in_pipe.close()
        self.out_pipe.close()

#------------------------------------------------------------------------------
class ModemHandle(io.ioloop.SockHandle):
    def __init__(self, pid, to_child, from_child, master, config):
        f2s_adapter = io.ioloop.FileToSockAdapter(from_child)
        io.ioloop.SockHandle.__init__(self, f2s_adapter)
        self.pid        = pid
        self.to_child   = to_child
        self.from_child = from_child
        self.ready = False
        self.master = master
        self.config = config

    def send_msg(self, msgpdu):
        if not self.ready:
            log.warn('sending sms to modem when not ready')
        self.ready = False
        self.send_cmd('s %s' % msgpdu)
        
    def send_cmd(self, cmd):
        self.to_child.write(cmd + '\n')
        self.to_child.flush()

    def fileno(self):
        return self.from_child.fileno()

    def recv_frame(self, l):
        if l == '':
            log.debug('modem process died')
            # XXX/bowei -- cleanup process
        elif l[0] == 'p':
            log.debug('got ping from %s' % self.config.name)
        elif l[0] == 'R':
            log.debug('got ready for msg from %s' % self.config.name)
            self.ready = True
        elif l[0] == 'r':
            _, raw_pdu = l.split()
            try:
                msg_pdu = pdu.Deliver_PDU.from_semioctet(raw_pdu)
            except Exception, e:
                log.error("couldn't decode PDU: %s" % e)
                return
            self.master.db.add_frag(msg_pdu.sender_num.num, msg_pdu.data.data)
            self.master.api.send_client_sms()

#------------------------------------------------------------------------------
class Master:
    def __init__(self, config):
        self.config  = config
        self.db      = msgdb.MessageDB(self, self.config.db_file)
        self.api     = api.API(self, self.config.api_addr)
        self.encoder = proto.Encoder(self.config.max_sms_len)

        self.processes = {}
        self.modems_to_restart = []
        
    def start_procs(self):
        for m in self.config.modems:
            self.start_modem(m, self.config.modems[m])

    def stop_procs(self):
        for name in self.processes:
            try:
                self.processes[name].send_cmd('Q')
            except IOError:
                pass

        dead = []
        while len(self.processes) > 0:
            sec_sleep(1)
            for name in self.processes:
                pid = self.processes[name].pid
                if os.waitpid(pid, os.WNOHANG) == (0,0):
                    log.warn('modem child process %s slow to quit...killing' % pid)
                    os.kill(pid, signal.SIGKILL)
                dead.append(name)
            for i in dead:
                del self.processes[i]
                    

    def start_modem(self, name, cfg):
        log.info('starting modem %s' % name)
        R, W = range(2)
        to_child = os.pipe()
        from_child = os.pipe()

        pid = os.fork()
        if pid:
            os.close(to_child[R])
            os.close(from_child[W])
            handle = ModemHandle(pid,
                                 os.fdopen(to_child[W], 'w'),
                                 os.fdopen(from_child[R]),
                                 self, cfg)
            self.processes[name] = handle
            io.ioloop.master_loop.add_handle(handle)
            log.info('added modem process %s' % name)
        else:
            os.close(to_child[W])
            os.close(from_child[R])

            p = ModemProcess(cfg,
                             os.fdopen(to_child[R]),
                             os.fdopen(from_child[W], 'w'))
            log.info('running modem process %s' % name)
            try:
                p.run()
            except KeyboardInterrupt:
                log.info('keyboard interrupt, exiting...')
                sys.exit(0)
            finally:
                p.close_all()

    def send_sms(self, modem, number, uniq, msg):
        if modem not in self.processes:
            log.error('trying to send to non-existant modem %s' % modem)
            return
        sms_list = self.encoder.encode(uniq, msg)
        for s in sms_list:
            p = pdu.pdu_7bit(number, s).as_semioctet()
            self.processes[modem].send_msg(p)
        
    def run(self):
        log.info('started')
        io.ioloop.master_loop.run()
#         while True:
#             t_a = time.time()
#             r, _, _ = select.select(self.processes.values() +
#                                     self.api.socks(), [], [], interval)
#             t_b = time.time()
#             log.debug("select returned %s after %f" % (r, t_b - t_a))
#             interval = max(interval - (t_b - t_a), 0)

#             if len(r) == 0:
#                 # timeout
#                 for p in self.processes:
#                     self.processes[p].send_cmd('r')
#                 for name, cfg in self.modems_to_restart:
#                     self.start_modem(name, cfg)
#                 interval = CHECK_INTERVAL
#                 self.modems_to_restart = []

#             dead = []
#             for sock in r:
#                 if sock == self.api.server_sock:
#                     self.api.accept()
#                 elif sock == self.api.client_sock:
#                     self.api.handle_response()
#                 else:
#                     r = sock.handle_response()
#                     if not r:
#                         found = False
#                         for i in self.processes:
#                             if sock == self.processes[i]:
#                                 log.warn('modem controlling process died')
#                                 self.modems_to_restart.append((i, sock.config))
#                                 found = True
#                                 dead.append(i)
#                         if not found:
#                             log.error('dead modem process not found')
#             for i in dead:
#                 del self.processes[i]

#------------------------------------------------------------------------------
# Run
#
if '--debug' in sys.argv:
    level = logging.DEBUG
else:
    level = logging.INFO

logging.basicConfig(level = level,
                    format = '%(asctime)s %(levelno)3s %(name)12s %(message)s')

cfg = Config()
cfg.parse_args()
master = Master(cfg)
master.start_procs()
try:
    master.run()
except KeyboardInterrupt:
    log.info('got keyboard interrupt, exiting...')
    sys.exit(0)
finally:
    master.stop_procs()
